package cn.edu.hitwh.kanlema.util;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RBloomFilter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

@Component
@RequiredArgsConstructor
public class RedisCache {
    private final RedisTemplate<String, String> redisTemplate;

    public boolean exists(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    public void setValue(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public void setValue(String key, String value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    public void setObject(String key, Object value) {
        redisTemplate.opsForValue().set(key, value == null ? "" : JSONUtil.toJsonStr(value));
    }

    public void setWithLogicalExpire(String key, ExpirationWrapper wrapper) {
        Objects.requireNonNull(wrapper.getValue());
        Objects.requireNonNull(wrapper.getExpireTime());
        redisTemplate.opsForHash().putAll(key, BeanUtil.beanToMap(wrapper, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, valueObj) -> valueObj == null ? null : valueObj.toString())
        ));
    }

    public void setWithLogicalExpire(String key, String value, long timeout, TimeUnit unit) {
        setWithLogicalExpire(key, new ExpirationWrapper(value, timeout, unit));
    }

    public void setWithLogicalExpire(String key, Object value, long timeout, TimeUnit unit) {
        setWithLogicalExpire(key, ExpirationWrapper.asJson(value, timeout, unit));
    }

    public void expire(String key, long timeout, TimeUnit unit) {
        redisTemplate.expire(key, timeout, unit);
    }

    public Duration getExpire(String key) {
        Long expire = redisTemplate.getExpire(key, TimeUnit.NANOSECONDS);
        if (expire == null || expire < 0) {
            return null;
        }
        return Duration.ofNanos(expire);
    }

    public String getValue(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public <T> T getObject(String key, Class<T> clazz) {
        String jsonStr = redisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(jsonStr)) {
            return null;
        }
        return JSONUtil.toBean(jsonStr, clazz);
    }

    public ExpirationWrapper getWithLogicalExpire(String key)  {
        Map<Object, Object> map = redisTemplate.opsForHash().entries(key);
        if (map.isEmpty()) {
            return null;
        }
        ExpirationWrapper wrapper = new ExpirationWrapper();
        BeanUtil.fillBeanWithMap(map, wrapper, false);
        return wrapper;
    }

    public String getWithLogicalExpire(String key, Runnable rebuildRunnable) {
        ExpirationWrapper wrapper = getWithLogicalExpire(key);
        if (wrapper == null) {
            return null;
        }
        LocalDateTime expireTime = wrapper.getExpireTime();
        if (expireTime.isBefore(LocalDateTime.now())) {
            rebuildRunnable.run();
        }
        return wrapper.getValue();
    }

    public <T> T getWithLogicalExpire(String key, Class<T> clazz, Runnable rebuildRunnable) {
        String value = getWithLogicalExpire(key, rebuildRunnable);
        if (value == null) {
            return null;
        }
        return JSONUtil.toBean(value, clazz);
    }

    public void delete(String key) {
        redisTemplate.delete(key);
    }

    public String getValueOrSet(String key, Supplier<String> supplier, long timeout, TimeUnit unit, long nullTimeout) {
        String value = getValue(key);
        if (value == null) {
            value = supplier.get();
            if (value != null) {
                setValue(key, value, timeout, unit);
            } else if (nullTimeout > 0) {
                setValue(key, "", nullTimeout, unit);
            }
        }
        return value;
    }

    public String getValueOrSet(String key, Supplier<String> supplier, long timeout, TimeUnit unit) {
        return getValueOrSet(key, supplier, timeout, unit, -1);
    }

    public <T> T getObjectOrSet(String key, Supplier<T> supplier, Class<T> clazz, long timeout, TimeUnit unit, long nullTimeout) {
        String json = getValue(key);
        T o = null;
        if (json == null) {
            o = supplier.get();
            if (o != null) {
                setObject(key, o);
                expire(key, timeout, unit);
            } else if (nullTimeout > 0) {
                setObject(key, null);
                expire(key, nullTimeout, unit);
            }
        } else if (StrUtil.isNotBlank(json)) {
            o = JSONUtil.toBean(json, clazz);
        }
        return o;
    }

    public <T> T getObjectOrSet(String key, Supplier<T> supplier, Class<T> clazz, long timeout, TimeUnit unit) {
        return getObjectOrSet(key, supplier, clazz, timeout, unit, -1);
    }

    public interface DataLoader<T> {
        List<T> load(Long start, Long count);
    }

    public static final long DEFAULT_BATCH_SIZE = 10000L;

    public static <T> void initBloomFilter(
            RBloomFilter<T> bloomFilter,
            long expectedInsertions,
            double falseProbability,
            DataLoader<T> dataLoader
    ) {
        if (bloomFilter.tryInit(expectedInsertions, falseProbability)) {
            for (long start = 0L; ; start += DEFAULT_BATCH_SIZE) {
                List<T> list = dataLoader.load(start, DEFAULT_BATCH_SIZE);
                if (list.isEmpty()) {
                    break;
                }
                list.forEach(bloomFilter::add);
            }
        }
    }

    // 一个允许由其它线程释放的分布式锁，用于实现逻辑过期
    public boolean tryLock(String key, long timeout, TimeUnit unit) {
        Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "1", timeout, unit);
        return Boolean.TRUE.equals(success);
    }

    // 释放锁
    public void unlock(String key) {
        redisTemplate.delete(key);
    }
}
